package com.nulldev.util.internal.backport.httpclient_rw.httpclient;

import static com.nulldev.util.web.HttpClient.util.HttpStreamUtil.__check_valid_buffer_size;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.List;
import java.util.Map;

import com.nulldev.util.data.Variables;
import com.nulldev.util.io.IOUtils;
import com.nulldev.util.io.throttling.BandwidthThrottledInputStream;
import com.nulldev.util.io.throttling.BandwidthThrottler;
import com.nulldev.util.web.HttpClient.HttpResponse;
import com.nulldev.util.web.HttpClient.ResponseFormat;
import com.nulldev.util.web.HttpClient.Version;
import com.nulldev.util.web.HttpClient.util.HttpStreamUtil;

public class BackportedHttpResponse<T> implements HttpResponse<T> {

	private final com.nulldev.util.internal.backport.httpclient_rw.HttpResponse<T> resp;
	private final ResponseFormat format;
	private final BandwidthThrottler throttler;
	private final boolean enable_os_monitoring;

	public BackportedHttpResponse(final com.nulldev.util.internal.backport.httpclient_rw.HttpResponse<T> resp, final ResponseFormat format,
			final BandwidthThrottler throttler, final boolean enable_os_monitoring) {
		this.resp = resp;
		this.format = format;
		this.throttler = throttler;
		this.enable_os_monitoring = enable_os_monitoring;
	}

	@Override
	public int status() {
		return this.resp.statusCode();
	}

	@Override
	public boolean successful() {
		return this.resp.statusCode() >= 200 && this.resp.statusCode() <= 299;
	}

	@Override
	public ResponseFormat format() {
		return this.format;
	}

	@SuppressWarnings("unchecked")
	@Override
	public T data() {
		BackportedHttpRequest.logger.debug("Response.data(): call");
		final String encoding = __get_encoding();
		switch (this.format) {
			case STREAM: {
				BackportedHttpRequest.logger.debug("Response.data(): STREAM");
				if (encoding == null) {
					if (this.throttler != null) {
						return (T) new BandwidthThrottledInputStream((InputStream) this.resp.body(), this.throttler);
					} else {
						return this.resp.body();
					}
				} else {
					try {
						if (this.throttler != null) {
							return (T) new BandwidthThrottledInputStream(HttpStreamUtil.decode((InputStream) this.resp.body(), encoding), this.throttler);
						} else {
							return (T) HttpStreamUtil.decode((InputStream) this.resp.body(), encoding);
						}
					} catch (IOException e) {
						throw new RuntimeException(e);
					}
				}
			}
			case READER: {
				if (encoding == null) {
					return (T) new InputStreamReader((InputStream) this.resp.body());
				} else {
					try {
						return (T) new InputStreamReader(HttpStreamUtil.decode((InputStream) this.resp.body(), encoding));
					} catch (IOException e) {
						throw new RuntimeException(e);
					}
				}
			}
			case STRING_VIA_STREAM: {
				if (encoding == null) {
					try {
						return (T) new String(IOUtils.toByteArray((InputStream) this.resp.body()));
					} catch (Exception e) {
						throw new RuntimeException(e);
					}
				} else {
					try {
						final InputStream output = HttpStreamUtil.decode((InputStream) this.resp.body(), encoding);
						BackportedHttpRequest.logger.debug("Response.data(): done.");
						return (T) new String(IOUtils.toByteArray(output));
					} catch (Exception e) {
						throw new RuntimeException(e);
					}
				}
			}
			default:
				return this.resp.body();
		}
	}

	@Override
	public void close() throws Exception {
		if (this.resp.body() instanceof InputStream) {
			((InputStream) this.resp.body()).close();
		}
		this.resp.close();
	}

	@Override
	public T dataThenClose() {
		final T data = this.data();
		try {
			this.adaptiveClose();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
		return data;
	}

	@Override
	public Map<String, List<String>> headers() {
		return this.resp.headers().map();
	}

	private final static String[] BLANK_ARR = new String[0];

	@Override
	public String[] cookies() {
		final List<String> o = this.resp.headers().map().get("set-cookie");
		if (o == null)
			return BLANK_ARR;
		return (String[]) o.toArray(new String[o.size()]);
	}

	@Override
	public T data(final boolean avoidDecoding) {
		if (avoidDecoding) {
			return this.resp.body();
		} else {
			return this.data();
		}
	}

	@Override
	public T dataThenClose(final boolean avoidDecoding) {
		final T data = this.data(avoidDecoding);
		try {
			close();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
		return data;
	}

	@Override
	public Version transportVersion() {
		switch (this.resp.version()) {
			case HTTP_1_0:
				return Version.HTTP_1_0;
			case HTTP_1_1:
				return Version.HTTP_1_1;
			case HTTP_2:
				return Version.HTTP_2_0;
			default:
				throw new UnsupportedOperationException("Unknown version: " + this.resp.version());
		}
	}

	@Override
	public void adaptiveClose() throws Exception {
		if (this.resp.body() instanceof InputStream) {
			((InputStream) this.resp.body()).close();
		}
	}

	private String __get_encoding() {
		final String encoding;
		if (this.headers().containsKey("Content-Encoding")) {
			encoding = this.headers().get("Content-Encoding").get(0);
		} else if (this.headers().containsKey("content-encoding")) {
			encoding = this.headers().get("content-encoding").get(0);
		} else {
			encoding = null;
		}
		return encoding;
	}

	private InputStream __get_stream() throws IOException {
		final InputStream returned;
		final String encoding = __get_encoding();
		switch (this.format) {
			case STREAM:
			case READER:
			case STRING_VIA_STREAM:
				returned = HttpStreamUtil.decode((InputStream) this.resp.body(), encoding);
				break;
			case BYTES:
				returned = new ByteArrayInputStream((byte[]) this.resp.body());
				break;
			case STRING:
				returned = new ByteArrayInputStream(((String) this.resp.body()).getBytes());
				break;
			default:
				throw new UnsupportedOperationException("Cannot __get_stream() from: " + this.format);
		}

		if (this.enable_os_monitoring) {
			return new BackportInputStreamMonitored(returned);
		} else {
			return returned;
		}
	}

	@Override
	public void writeTo(final OutputStream os) throws IOException {
		Variables.requireNonNull(os, "os");
		InputStream is = __get_stream();
		if (this.throttler != null)
			is = new BandwidthThrottledInputStream(is, this.throttler);
		IOUtils.autoCopy(is, os);
		IOUtils.closeQuietly(is);
	}

	@Override
	public void writeTo(final OutputStream os, final int preferredBufferSize) throws IOException {
		Variables.requireNonNull(os, "os");
		__check_valid_buffer_size(preferredBufferSize);
		InputStream is = __get_stream();
		if (this.throttler != null)
			is = new BandwidthThrottledInputStream(is, this.throttler);
		IOUtils.autoCopyWithBufferSize(is, os, preferredBufferSize);
		IOUtils.closeQuietly(is);
	}

	@Override
	public void writeTo(final WritableByteChannel os) throws IOException {
		Variables.requireNonNull(os, "os");
		InputStream is = __get_stream();
		if (this.throttler != null)
			is = new BandwidthThrottledInputStream(is, this.throttler);
		final ReadableByteChannel iso = IOUtils.copyMakeChannel(is);
		IOUtils.fastCopy(iso, os);
		IOUtils.closeQuietly(iso);
	}
}
